package com.tiantian.user.service;

import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.secure.BCrypt;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.tiantian.common.core.constant.CacheConstants;
import com.tiantian.common.core.constant.Constants;
import com.tiantian.common.core.constant.UserConstants;
import com.tiantian.common.core.domain.LoginUser;
import com.tiantian.common.core.domain.model.RoleDTO;
import com.tiantian.common.core.domain.model.SysRolePermVO;
import com.tiantian.common.core.exception.BusinessException;
import com.tiantian.common.core.utils.ServletUtils;
import com.tiantian.common.core.utils.SpringUtils;
import com.tiantian.common.core.utils.StringUtils;
import com.tiantian.common.log.event.LogininforEvent;
import com.tiantian.common.redis.utils.RedisUtils;
import com.tiantian.common.satoken.utils.LoginHelper;
import com.tiantian.user.domain.entity.SysRole;
import com.tiantian.user.domain.entity.SysRolePerm;
import com.tiantian.user.domain.entity.SysUser;
import com.tiantian.user.mapper.SysRolePermissionMapper;
import com.tiantian.user.mapper.SysUserMapper;
import com.tiantian.user.mapper.SysUserRoleMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.time.Duration;
import java.util.List;
import java.util.Set;
import java.util.function.Supplier;

/**
 * 登录校验方法
 */
@RequiredArgsConstructor
@Slf4j
@Service
public class SysLoginService {

    private final SysUserMapper userMapper;
    private final SysRolePermissionMapper sysRolePermissionMapper;
    private final SysUserRoleMapper sysUserRoleMapper;

    // 最大重试次数
    @Value("${user.password.maxRetryCount}")
    private Integer maxRetryCount;

    // 账号锁定时间
    @Value("${user.password.lockTime}")
    private Integer lockTime;

    /**
     * 登录验证
     *
     * @param username 用户名
     * @param password 密码
     * @return token
     */
    public String login(String username, String password) {
        // 根据用户名查询用户
        SysUser user = loadUserByUsername(username);
        // 使用checkpw方法检查被加密的字符串是否与原始字符串匹配
        checkLogin(user.getUserId(), username, () -> !BCrypt.checkpw(password, user.getPassword()));
        // 判断账号是否被冻结
        if (UserConstants.USER_DISABLE.equals(user.getStatus())) {
            throw new BusinessException(StrUtil.format("账号: {} 已被冻结,请联系管理员解除", username));
        }
        LoginUser loginUser = buildLoginUser(user);
        // 生成token
        LoginHelper.login(loginUser);
        // 记录信息
        return StpUtil.getTokenValue();
    }

    /**
     * 退出登录
     */
    public void logout() {
        try {
            LoginUser loginUser = LoginHelper.getLoginUser();
            if (ObjectUtil.isNull(loginUser)) {
                return;
            }
            // 记录用户退出的信息
            recordLogininfor(loginUser.getUserId(), loginUser.getUserName(), Constants.LOGOUT, "退出登录成功");
        } catch (NotLoginException ignored) {
        } finally {
            try {
                // 退出登录
                StpUtil.logout();
            } catch (NotLoginException ignored) {
            }
        }
    }

    /**
     * 根据账号查询用户是否存在
     *
     * @param username 账号
     * @return SysUser
     */
    private SysUser loadUserByUsername(String username) {
        // 查询出用户信息
        SysUser user = userMapper.selectUserByUserName(username);
        if (ObjectUtil.isNull(user)) {
            log.info("登录用户：{} 不存在.", username);
            throw new BusinessException("用户不存在");
        }
        return user;
    }

    /**
     * 构建登录用户
     */
    private LoginUser buildLoginUser(SysUser user) {
        // 属性拷贝
        LoginUser loginUser = BeanUtil.copyProperties(user, LoginUser.class);

        // 设置用户角色
        Long userId = user.getUserId();
        SysRole sysRole = sysUserRoleMapper.selectRoleByUserId(userId);
        RoleDTO roleDTO = BeanUtil.copyProperties(sysRole, RoleDTO.class);
        loginUser.setRoleDTO(roleDTO);

        // 设置角色权限
        Set<SysRolePerm> rolePermission = sysRolePermissionMapper.getRolePermissionByUserId(userId);
        List<SysRolePermVO> sysRolePermVO = BeanUtil.copyToList(rolePermission, SysRolePermVO.class);
        loginUser.setPermissions(sysRolePermVO);

        return loginUser;
    }

    /**
     * 登录校验<br/>
     * 1 密码错误超过5次账号进行锁定<br/>
     * 2 查询账号是否被锁定<br/>
     */
    private void checkLogin(Long userId, String username, Supplier<Boolean> supplier) {
        String errorKey = CacheConstants.PWD_ERR_CNT_KEY + username;
        String loginFail = Constants.LOGIN_FAIL;
        // 获取用户登录错误次数，查询为空时默认为0
        int errorNumber = ObjectUtil.defaultIfNull(RedisUtils.getCacheObject(errorKey), 0);
        // 锁定时间内登录 则踢出
        if (errorNumber >= maxRetryCount) {
            recordLogininfor(userId, username, loginFail, "锁定时间内登录, 账号已被锁定" + lockTime + "秒");
            throw new BusinessException(StringUtils.format("锁定时间内不能登录, 重试次数超过{}次, 账号已被锁定{}秒", maxRetryCount, lockTime));
        }

        // 如果密码与数据库密码不匹配
        if (supplier.get()) {
            // 错误次数递增
            errorNumber++;
            RedisUtils.setCacheObject(errorKey, errorNumber, Duration.ofMinutes(lockTime));
            // 达到规定错误次数 则锁定登录
            if (errorNumber >= maxRetryCount) {
                String errorMessage = StrUtil.format("重试次数超过{}次, 账号已被锁定{}秒", maxRetryCount, lockTime);
                recordLogininfor(userId, username, loginFail, errorMessage);
                throw new BusinessException(errorMessage);
            } else {
                // 未达到规定错误次数
                recordLogininfor(userId, username, loginFail, StrUtil.format("密码错误, 当前重试次数:{}, 最大重试次数:{}", errorNumber, maxRetryCount));
                throw new BusinessException("账号或者密码错误, 剩余重试次数:" + (maxRetryCount - errorNumber));
            }
        }

        // 登录成功 清空错误次数
        RedisUtils.deleteObject(errorKey);
    }

    /**
     * 记录登录信息
     *
     * @param username 用户名
     * @param status   状态
     */
    public void recordLogininfor(Long userId, String username, String status, String message) {
        // 封装对象
        LogininforEvent logininforEvent = new LogininforEvent();
        logininforEvent.setUserId(userId);
        logininforEvent.setUserName(username);
        logininforEvent.setStatus(status);
        logininforEvent.setMessage(message);
        logininforEvent.setMessage("退出登录成功");
        logininforEvent.setRequest(ServletUtils.getRequest());
        SpringUtils.context().publishEvent(logininforEvent);
    }
}
